/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "TSocketServer.h"
#import <Foundation/Foundation.h>
#include <netinet/in.h>
#import <sys/socket.h>
#import "TNSFileHandleTransport.h"
#import "TObjective-C.h"
#import "TProtocol.h"
#import "TTransportException.h"

NSString* const
    kTSocketServer_ClientConnectionFinishedForProcessorNotification =
        @"TSocketServer_ClientConnectionFinishedForProcessorNotification";
NSString* const kTSocketServer_ProcessorKey = @"TSocketServer_Processor";
NSString* const kTSockerServer_TransportKey = @"TSockerServer_Transport";

@implementation TSocketServer

- (id)initWithPort:(int)port
     protocolFactory:(id<TProtocolFactory>)protocolFactory
    processorFactory:(id<TProcessorFactory>)processorFactory;
{
  self = [super init];

  mInputProtocolFactory = [protocolFactory retain_stub];
  mOutputProtocolFactory = [protocolFactory retain_stub];
  mProcessorFactory = [processorFactory retain_stub];

  // create a socket.
  int fd = -1;
  CFSocketRef socket = CFSocketCreate(
      kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL);
  if (socket) {
    CFSocketSetSocketFlags(
        socket, CFSocketGetSocketFlags(socket) & ~kCFSocketCloseOnInvalidate);
    fd = CFSocketGetNative(socket);
    int yes = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_len = sizeof(addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    NSData* address = [NSData dataWithBytes:&addr length:sizeof(addr)];
    if (CFSocketSetAddress(socket, (bridge_stub CFDataRef)address) !=
        kCFSocketSuccess) {
      CFSocketInvalidate(socket);
      CFRelease(socket);
      NSLog(@"*** Could not bind to address");
      return nil;
    }
  } else {
    NSLog(@"*** No server socket");
    return nil;
  }

  // wrap it in a file handle so we can get messages from it
  mSocketFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fd
                                                    closeOnDealloc:YES];

  // throw away our socket
  CFSocketInvalidate(socket);
  CFRelease(socket);

  // register for notifications of accepted incoming connections
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(connectionAccepted:)
             name:NSFileHandleConnectionAcceptedNotification
           object:mSocketFileHandle];

  // tell socket to listen
  [mSocketFileHandle acceptConnectionInBackgroundAndNotify];

  NSLog(@"Listening on TCP port %d", port);

  return self;
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [mInputProtocolFactory release_stub];
  [mOutputProtocolFactory release_stub];
  [mProcessorFactory release_stub];
  [mSocketFileHandle release_stub];
  [super dealloc_stub];
}

- (void)connectionAccepted:(NSNotification*)aNotification {
  NSFileHandle* socket = [[aNotification userInfo]
      objectForKey:NSFileHandleNotificationFileHandleItem];

  // now that we have a client connected, spin off a thread to handle activity
  [NSThread detachNewThreadSelector:@selector(handleClientConnection:)
                           toTarget:self
                         withObject:socket];

  [[aNotification object] acceptConnectionInBackgroundAndNotify];
}

- (void)handleClientConnection:(NSFileHandle*)clientSocket {
#if __has_feature(objc_arc)
  @autoreleasepool {
    TNSFileHandleTransport* transport =
        [[TNSFileHandleTransport alloc] initWithFileHandle:clientSocket];
    id<TProcessor> processor =
        [mProcessorFactory processorForTransport:transport];

    id<TProtocol> inProtocol =
        [mInputProtocolFactory newProtocolOnTransport:transport];
    id<TProtocol> outProtocol =
        [mOutputProtocolFactory newProtocolOnTransport:transport];

    @try {
      BOOL result = NO;
      do {
        @autoreleasepool {
          result = [processor processOnInputProtocol:inProtocol
                                      outputProtocol:outProtocol];
        }
      } while (result);
    } @catch (TTransportException* te) {
      // NSLog(@"Caught transport exception, abandoning client connection: %@",
      // te);
    }

    NSNotification* n = [NSNotification
        notificationWithName:
            kTSocketServer_ClientConnectionFinishedForProcessorNotification
                      object:self
                    userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
                                               processor,
                                               kTSocketServer_ProcessorKey,
                                               transport,
                                               kTSockerServer_TransportKey,
                                               nil]];
    [[NSNotificationCenter defaultCenter]
        performSelectorOnMainThread:@selector(postNotification:)
                         withObject:n
                      waitUntilDone:YES];
  }
#else
  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

  TNSFileHandleTransport* transport =
      [[TNSFileHandleTransport alloc] initWithFileHandle:clientSocket];
  id<TProcessor> processor =
      [mProcessorFactory processorForTransport:transport];

  id<TProtocol> inProtocol =
      [[mInputProtocolFactory newProtocolOnTransport:transport] autorelease];
  id<TProtocol> outProtocol =
      [[mOutputProtocolFactory newProtocolOnTransport:transport] autorelease];

  @try {
    BOOL result = NO;
    do {
      NSAutoreleasePool* myPool = [[NSAutoreleasePool alloc] init];
      result = [processor processOnInputProtocol:inProtocol
                                  outputProtocol:outProtocol];
      [myPool release];
    } while (result);
  } @catch (TTransportException* te) {
    // NSLog(@"Caught transport exception, abandoning client connection: %@",
    // te);
  }

  NSNotification* n = [NSNotification
      notificationWithName:
          kTSocketServer_ClientConnectionFinishedForProcessorNotification
                    object:self
                  userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
                                             processor,
                                             kTSocketServer_ProcessorKey,
                                             transport,
                                             kTSockerServer_TransportKey,
                                             nil]];
  [[NSNotificationCenter defaultCenter]
      performSelectorOnMainThread:@selector(postNotification:)
                       withObject:n
                    waitUntilDone:YES];

  [pool release];
#endif
}

@end
